<?php
/**
 * @category Goatherd
 * @package Goatherd
 *
 * @author Copyright (c) 2012 Maik Penz <maik@phpkuh.de>
 * @version $Id: Unicode.php 77 2012-03-27 07:53:54Z maik@phpkuh.de $
 *
 * This file is part of Goatherd library.
 *
 * Goatherd library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Goatherd library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Goatherd library. If not, see <http://www.gnu.org/licenses/>.
 */
namespace Goatherd\Convert;

/**
 * Basic latin1-utf8 converter.
 *
 * @note likely only useful from some iso-8859-1 derivates.
 * @note Detect order is allways substituted.
 * @note Only works on Unicode Base Multilingual Plane.
 *
 * Substitution character and language/ locale should be set correctly.
 *
 * @todo abstract encoding converter interface
 * @todo allow setting of default encoding, detect order and enc map(s)
 * @todo use flags on str* methods in order to abstract sanitasations: normalise, convert case, encode
 * @todo (try to) use __call to simplify similar str* methods
 * @todo allow to invoke str* static
 *
 * @category Goatherd
 * @package Goatherd
 * @subpackage Convert
 */
class Unicode
{
    const FIX_NONE           = 0;
    const FIX_CP_1252        = 1;
    const FIX_ISO_8859_15    = 2;

    /**
     * "iso-8859-1" is a fallback as it will always be detected if "auto" fails.
     *
     * @var string
     */
    protected $_detectOrder = 'auto, iso-8859-1';

    /**
     * Untested for other unicode variants.
     *
     * @var string
     */
    protected $_defaultEncoding = 'utf-8';

    /**
     * Maps code points from CP 1252 to
     * iso-8859-1 control codes (#128-#159)
     *
     * @var array
     */
    protected $_fix = array(
        self::FIX_CP_1252 => array(
            // windows-1252 | cp 1252
            '&#128;'	=> '&#8364;',    // €
            '&#130;'	=> '&#8218;',    // ‚
            '&#131;'	=> '&#402;',     // ƒ
            '&#132;'	=> '&#8222;',    // „
            '&#133;'	=> '&#8230;',    // …
            '&#134;'	=> '&#8224;',    // †
            '&#135;'	=> '&#8225;',    // ‡
            '&#136;'	=> '&#710;',     // ˆ
            '&#137;'	=> '&#8240;',    // ‰
            '&#138;'	=> '&#352;',     // Š
            '&#139;'	=> '&#8249;',    // ‹
            '&#140;'	=> '&#338;',     // Œ
            '&#142;'	=> '&#381;',     // Ž
            '&#145;'	=> '&#8216;',    // ‘
            '&#146;'	=> '&#8217;',    // ’
            '&#147;'	=> '&#8220;',    // “
            '&#148;'	=> '&#8221;',    // ”
            '&#149;'	=> '&#8226;',    // •
            '&#150;'	=> '&#8211;',    // –
            '&#151;'	=> '&#8212;',    // —
            '&#152;'	=> '&#732;',     // ˜
            '&#153;'	=> '&#8482;',    // ™
            '&#154;'	=> '&#353;',     // š
            '&#155;'	=> '&#8250;',    // ›
            '&#156;'	=> '&#339;',     // œ
            '&#158;'	=> '&#382;',     // ž
            '&#159;'	=> '&#376;',     // Ÿ
        ),
        self::FIX_ISO_8859_15 => array(
            '&#164;'	=> '&#8364;',    // €
            '&#166;'	=> '&#0352;',    // Š
            '&#168;'	=> '&#0353;',    // š
            '&#180;'	=> '&#0381;',    // Ž
            '&#184;'	=> '&#0382;',    // ž
            '&#188;'	=> '&#0338;',    // Œ
            '&#189;'	=> '&#0339;',    // œ
            '&#190;'	=> '&#0376;',    // Ÿ
        )
    );

    /**
     * Convert all codes points beyond #127 to numeric html entities
     * (all codes outside of 'ascii' scope)
     *
     * @var array
     */
    protected $_encIso8859Convmap = array(0x80, 0xFFFF, 0, 0xFFFF);

    public function getEncMap($flag)
    {
        $encmap = array();
        for ($bitmap = 1; $flag > 0; $flag = $flag >> 1) {
            // flag set?
            if ($flag%2==1) {
                foreach ($this->_fix[$bitmap] as $key => $val) {
                    // TODO: notice if key is set
                    $encmap[$key] = $val;
                }
            }
            // step bit pointer
            $bitmap = $bitmap << 1;
        }

        return $encmap;
    }

    /**
     * Convert string encoding.
     *
     * @note mb_string/ iconv can not distinguish between the various latin1 encodings.
     * This algorithm is designed to support basic ISO-8859-1.
     * CP 1252 is enabled by default as those two seem to be quite common and
     * are compatible for printable text. (they differ in a range of control
     * characters that are rarely used).
     * Support for ISO-8859-15 might be added by the flag, but might produce
     * conflict for some rare characters. It is mostly compatible with ISO-8859-1
     * and CP 1252.
     *
     * @param string 		$content
     * @param string|null 	$encoding		[=null; autodetect]
     * @param string 		$outEncoding	[=null; 'UTF-8']
     * @param int			$fix			[=self::FIX_CP_1252]
     *
     * @return string
     */
    public function convert($content, $in_encoding = null, $out_encoding = null, $fix = self::FIX_CP_1252)
    {
        // not handle empty content
        if (!$content) {
            return (string)$content;
        }
        // detect encoding (if none provided)
        if (!is_string($in_encoding)) {
            // will allways detect iso-8859-1 if auto fails..
            $in_encoding = mb_detect_encoding($content, $this->_detectOrder , true);
        }
        // out encoding
        null === $out_encoding and $out_encoding = $this->_defaultEncoding;

        // fix case
        $in_encoding = strtoupper($in_encoding);
        $out_encoding = strtoupper($out_encoding);

        // any need to convert?
        if ($in_encoding === $out_encoding) {
            // TODO: study this again...
            $content = mb_encode_numericentity($content, $this->_encIso8859Convmap, $in_encoding);
            $content = mb_convert_encoding($content, $out_encoding, 'HTML-ENTITIES');
            return $content;
        }
        if ($in_encoding === 'ISO-8859-1') {
            // convert all 'non-ascii' characters to numeric entities
            $content = mb_encode_numericentity($content, $this->_encIso8859Convmap, $in_encoding);

            // fix some numeric entities
            $encmap = $this->getEncMap($fix);
            $content = str_replace(array_keys($encmap), $encmap, $content);

            // transform back to desired encoding
            $content = mb_convert_encoding($content, $out_encoding, 'HTML-ENTITIES');
        } else {
            // TODO: validate for similar errors (i.e. further non-detectable encodings)
            $content = mb_convert_encoding($content, $out_encoding, $in_encoding);
        }

        return $content;
    }

    /**
     * Case-insensitive comparision based on normalized utf-8 encoding.
     *
     * @param string		$str1
     * @param string		$str2
     *
     * @return int
     */
    public function stricmp($str1, $str2)
    {
        return $this->strcmp($str1, $str2, true);
    }

    /**
     * Comparision based on normalized utf-8 encoding.
     *
     * @param string		$str1
     * @param string		$str2
     * @param boolean		$ignoreCase
     *
     * @return int
     */
    public function strcmp($str1, $str2, $ignoreCase = false)
    {
        $str1 = $this->prepareStringForComparision($str1, $ignoreCase);
        $str2 = $this->prepareStringForComparision($str2, $ignoreCase);

        return strcmp($str1, $str2);
    }

    public function strcoll($str1, $str2, $ignoreCase = false)
    {
        $str1 = $this->prepareStringForComparision($str1, $ignoreCase);
        $str2 = $this->prepareStringForComparision($str2, $ignoreCase);

        return strcoll($str1, $str2);
    }

    public function prepareStringForComparision($str, $ignoreCase = false)
    {
        $str = $this->convert($str1);

        $str = $this->normalize($str1);

        if ($ignoreCase) {
            $str = mb_convert_case($str1, MB_CASE_LOWER);
        }

        return $str;
    }

    public function str_split($str, $split_length = 1, $encode = false)
    {
        $encode and $str = $this($str);
        $n = (int) $split_length;
        return preg_split('/(\p{any}{$n,$n})/u', $str, -1, PREG_SPLIT_DELIM_CAPTURE );
    }

    /**
     *
     * @param string		$content
     * @param unknown_type	$norm
     * @return string
     */
    public function normalize($content, $norm = \Normalizer::FORM_C)
    {
        if (!\Normalizer::isNormalized ($content, $norm)) {
            $c = \Normalizer::normalize($content, $norm);
            if (!empty($c)) {
                return $c;
            }
            // TODO: warn if failed to normalize string!
        }

        return $content;
    }

    /**
     * Shorthand for transform(string[, null, null, FIX_CP_1252]).
     *
     * @param string $text
     * @return string
     */
    public function __invoke($text)
    {
        return $this->convert($text);
    }
}
